// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using ILLink.Shared.TypeSystemProxy;
using Mono.Cecil;

namespace Mono.Linker
{
    public static class MethodReferenceExtensions
    {
        public static string GetDisplayName(this MethodReference method)
        {
            var sb = new System.Text.StringBuilder();

            // Match C# syntaxis name if setter or getter
#pragma warning disable RS0030 // Cecil's Resolve is banned -- this should be a very cold path and makes calling this method much simpler
            var methodDefinition = method.Resolve();
#pragma warning restore RS0030
            if (methodDefinition != null && (methodDefinition.IsSetter || methodDefinition.IsGetter))
            {
                // Append property name
                string name = methodDefinition.IsSetter ? string.Concat(methodDefinition.Name.AsSpan(4), ".set") : string.Concat(methodDefinition.Name.AsSpan(4), ".get");
                sb.Append(name);
                // Insert declaring type name and namespace
                sb.Insert(0, '.').Insert(0, method.DeclaringType.GetDisplayName());
                return sb.ToString();
            }

            if (methodDefinition != null && methodDefinition.IsEventMethod())
            {
                // Append event name
                string name = methodDefinition.SemanticsAttributes switch
                {
                    MethodSemanticsAttributes.AddOn => string.Concat(methodDefinition.Name.AsSpan(4), ".add"),
                    MethodSemanticsAttributes.RemoveOn => string.Concat(methodDefinition.Name.AsSpan(7), ".remove"),
                    MethodSemanticsAttributes.Fire => string.Concat(methodDefinition.Name.AsSpan(6), ".raise"),
                    _ => throw new NotSupportedException(),
                };
                sb.Append(name);
                // Insert declaring type name and namespace
                sb.Insert(0, '.').Insert(0, method.DeclaringType.GetDisplayName());
                return sb.ToString();
            }

            // Append parameters
            sb.Append('(');
            if (method.HasMetadataParameters())
            {
#pragma warning disable RS0030 // MethodReference.Parameters is banned -- it's best to leave this as is for now
                for (int i = 0; i < method.Parameters.Count - 1; i++)
                    sb.Append(method.Parameters[i].ParameterType.GetDisplayNameWithoutNamespace()).Append(", ");
                sb.Append(method.Parameters[method.Parameters.Count - 1].ParameterType.GetDisplayNameWithoutNamespace());
#pragma warning restore RS0030 // Do not used banned APIs
            }

            sb.Append(')');

            // Insert generic parameters
            if (method.HasGenericParameters)
            {
                TypeReferenceExtensions.PrependGenericParameters(method.GenericParameters, sb);
            }

            // Insert method name
            if (method.Name == ".ctor")
                sb.Insert(0, method.DeclaringType.Name);
            else
                sb.Insert(0, method.Name);

            // Insert declaring type name and namespace
            if (method.DeclaringType != null)
                sb.Insert(0, '.').Insert(0, method.DeclaringType.GetDisplayName());

            return sb.ToString();
        }

        public static TypeReference GetReturnType(this MethodReference method)
        {
            if (method.DeclaringType is GenericInstanceType genericInstance)
                return TypeReferenceExtensions.InflateGenericType(genericInstance, method.ReturnType);

            return method.ReturnType;
        }

        public static bool ReturnsVoid(this IMethodSignature method)
        {
            return method.ReturnType.WithoutModifiers().MetadataType == MetadataType.Void;
        }

        public static TypeReference GetInflatedParameterType(this MethodReference method, int parameterIndex)
        {
#pragma warning disable RS0030 // MethodReference.Parameters is banned -- it's best to leave this as is for now
            IGenericInstance? genericInstance = method as IGenericInstance ?? method.DeclaringType as IGenericInstance;
            if (genericInstance is null)
                return method.Parameters[parameterIndex].ParameterType;
            return TypeReferenceExtensions.InflateGenericType(genericInstance, method.Parameters[parameterIndex].ParameterType);
#pragma warning restore RS0030 // Do not used banned APIs
        }

        /// <summary>
        /// Gets the number of entries in the 'Parameters' section of a method's metadata (i.e. excludes the implicit 'this' from the count)
        /// </summary>
#pragma warning disable RS0030 // MethodReference.Parameters is banned -- this provides a wrapper
        public static int GetMetadataParametersCount(this MethodReference method)
            => method.Parameters.Count;
#pragma warning restore RS0030 // Do not used banned APIs

        /// <summary>
        /// Returns true if the method has any parameters in the .parameters section of the method's metadata (i.e. excludes the impicit 'this')
        /// </summary>
        public static bool HasMetadataParameters(this MethodReference method)
            => method.GetMetadataParametersCount() != 0;

        /// <summary>
        /// Returns the number of the parameters pushed before the method's call (i.e. including the implicit 'this' if present)
        /// </summary>
#pragma warning disable RS0030 // MethodReference.Parameters is banned -- this provides a wrapper
        public static int GetParametersCount(this MethodReference method)
            => method.Parameters.Count + (method.HasImplicitThis() ? 1 : 0);
#pragma warning restore RS0030 // Do not used banned APIs

        public static bool IsDeclaredOnType(this MethodReference method, string fullTypeName)
        {
            return method.DeclaringType.IsTypeOf(fullTypeName);
        }

        public static bool HasImplicitThis(this MethodReference method)
        {
            return method.HasThis && !method.ExplicitThis;
        }

        /// <summary>
        /// Returns an IEnumerable of the ReferenceKind of each parameter, with the first being for the implicit 'this' parameter if it exists
        /// Used for better performance when it's only necessary to get the ReferenceKind of all parameters and nothing else.
        /// </summary>
        public static IEnumerable<ReferenceKind> GetParameterReferenceKinds(this MethodReference method)
        {
            if (method.HasImplicitThis())
                yield return method.DeclaringType.IsValueType ? ReferenceKind.Ref : ReferenceKind.None;
#pragma warning disable RS0030 // MethodReference.Parameters is banned -- this provides a wrapper
            foreach (var parameter in method.Parameters)
                yield return GetReferenceKind(parameter);
#pragma warning restore RS0030 // Do not used banned APIs

            static ReferenceKind GetReferenceKind(ParameterDefinition param)
            {
                if (!param.ParameterType.IsByReference)
                    return ReferenceKind.None;
                if (param.IsIn)
                    return ReferenceKind.In;
                if (param.IsOut)
                    return ReferenceKind.Out;
                return ReferenceKind.Ref;
            }
        }
    }
}
